﻿using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
using System.Collections.Generic;
using System.Xml.Serialization;
using weBoard;
using Brush = System.Windows.Media.Brush;
using Point = System.Windows.Point;

namespace weBoard
{
    #region Enums
    /// <summary>
    /// Types of operational objects
    /// </summary>
    public enum weObjType
    {
        ObjPoint,
        ObjEllipse,
        ObjLine,
        ObjRect,
        ObjPath,
        ObjPolygon,
        ObjPolyline
    };

    /// <summary>
    /// Types of possible actions
    /// </summary>
    public enum weActType
    {
        Create,
        Delete,
        Move,
        Resize,
        Recolor
    }
    #endregion Enums

    #region Serializable classes
    /// <summary>
    /// Represents record of an action
    /// </summary>
    [Serializable]
    public class weBoardAction
    {
        [XmlElement("Act")]
        public weActType Act;
        [XmlElement("Object")]
        public weObjType Object;
        [XmlElement("Uid")]
        public String Uid;
        [XmlElement("Was")]
        public weBoardState Was;
        [XmlElement("Now")]
        public weBoardState Now;
        [XmlElement("Record")]
        public bool Record;
    }

    /// <summary>
    /// State of a graphical object
    /// </summary>
    [Serializable]
    [XmlInclude(typeof(SolidColorBrush))]
    [XmlInclude(typeof(MatrixTransform))]
    public class weBoardState
    {
        [XmlElement("CoordsA")]
        public Point CoordsA;
        [XmlElement("CoordsB")]
        public Point CoordsB;
        [XmlElement("Fill")]
        public Brush Fill;
        [XmlElement("Width")]
        public double Width;
        [XmlElement("Height")]
        public double Height;
        [XmlElement("Text")]
        public String Text;
        [XmlElement("MetaProperties")]
        public object MetaProperties;
    }
    #endregion Serializable classes
}

namespace weBoardSvr
{

    /// <summary>
    /// Performs server part of a protocol
    /// </summary>
    public partial class weBoardServer
    {
        #region Private variables

        private string _extAddr;

        private static volatile Label _lblCount; //Label with clients count

        private bool _inetAlive;

        private Thread _serverThread;

        private Thread _checkExtIpThread;

        private static EventHandler _extHandler; //for autoupdating gui (label with IP after resolving)

        private bool _working; //is server working

        private TcpListener _serverSocket;

        private TcpClient _clientSocket;

        private const int DefPort = 1234;

        private string _hostname;

        private int _port;

        #endregion Private variables

        #region Public members

        /// <summary>
        /// Actions for window with current picture
        /// </summary>
        public static readonly List<weBoardAction> Actions = new List<weBoardAction>();

        /// <summary>
        /// Clients
        /// </summary>
        public readonly Hashtable PoolClients = new Hashtable();

        /// <summary>
        /// Casts message to clients
        /// </summary>
        /// <param name="msg">Message</param>
        /// <param name="nick">Nickname (initiator)</param>
        /// <param name="flag">Targets: 0 == broadcast (command), 1 == multicast (info or command to all but Nick), 2 == unicast (info or command for Nick only)</param>
        public void Cast(string msg, string nick, int flag)
        {
            if (msg == String.Empty)
                return;
            lock (PoolClients)
            {
                foreach (DictionaryEntry item in PoolClients)
                {
                    if (nick == (string) item.Key && flag == 1)
                        continue;
                    if (nick != (string) item.Key && flag == 2)
                        continue;
                    var castSocket = ((poolClient) item.Value).ClientSocket;
                    if (!castSocket.Connected)
                        continue;
                    var castStream = castSocket.GetStream();
                    if (null == castStream)
                        continue;
                    WriteString("#" + msg + ":" + nick + "$", castStream);
                    Debug.WriteLine(msg);
                    if ((string) item.Key == nick && flag == 2)
                        return;
                }
            }
        }

        /// <summary>
        /// Sets controls for interaction with app GUI
        /// </summary>
        /// <param name="lbl">Label</param>
        public static void SetGuiElements(Label lbl)
        {
            if (lbl != null)
                _lblCount = lbl;
        }

        /// <summary>
        /// Updates label showing clients count
        /// </summary>
        public void UpdateClientsCountGui()
        {
            if (_lblCount != null)
                Application.Current.Dispatcher.BeginInvoke(new Action(() => { _lblCount.Content = PoolClients.Count; }),
                    DispatcherPriority.Normal);
        }

        /// <summary>
        /// Disconnects client
        /// </summary>
        /// <param name="nick">Nick of client</param>
        public void DisconnectClient(string nick)
        {
            if (!PoolClients.ContainsKey(nick))
                return;
            lock (PoolClients)
            {
                ((poolClient) PoolClients[nick]).Stop();
            }
            Cast("LEAVE:" + nick, nick, 1);
        }

        /// <summary>
        /// Retranslates action
        /// </summary>
        /// <param name="act">Action</param>
        /// <param name="nick">Client to exclude</param>
        public void Retranslate(weBoardAction act, string nick)
        {
            Actions.Add(act);
            Cast(ObjToByteArray(act), nick, 1);
        }

        /// <summary>
        /// Stops server and so on...
        /// </summary>
        public void Stop(string reason)
        {
            Debug.WriteLine("Stop::Reason: " + reason);
            if (_working)
            {
                if (_serverThread.IsAlive)
                {
                    Cast("DSCALL", "SV", 0);
                    Debug.WriteLine("Stop::aborting thread (is alive)");
                    _serverThread.Abort();
                }
                var nicks = new List<string>();
                lock (PoolClients)
                {
                    foreach (String nick in PoolClients.Keys)
                        nicks.Add(nick);
                    foreach (var nick in nicks)
                        DisconnectClient(nick);
                    PoolClients.Clear();
                    UpdateClientsCountGui();
                }
                if (_clientSocket != null)
                    _clientSocket.Close();
                if (_serverSocket != null)
                    _serverSocket.Stop();
                _working = false;
            }
            else
            {
                if (null != _serverThread)
                    if (_serverThread.IsAlive)
                    {
                        Debug.WriteLine("Stop::Aborting thread (still alive) but the server is in non-working state");
                        _serverThread.Abort();
                    }
            }
        }

        /// <summary>
        /// Starts server and so on
        /// </summary>
        public void Start(string host, Int32 port)
        {
            _hostname = host;
            _port = port;
            Actions.Clear();
            try
            {
                if (!_working)
                {
                    if (null != _serverThread)
                        if (_serverThread.IsAlive)
                        {
                            Debug.WriteLine("Start::aborting thread (still alive)");
                            _serverThread.Abort();
                        }
                    _serverSocket = new TcpListener(IPAddress.Parse(_hostname), _port);
                    _clientSocket = default(TcpClient);
                    _serverSocket.Start();
                    _serverThread = new Thread(Listen) { Name = "ServerListen_Thread" };
                    _serverThread.Start();
                    _working = true;
                    Debug.WriteLine("Server started...");
                }
                else
                {
                    Debug.WriteLine("Server was started already...");
                }

            }
            catch (Exception ex)
            {
                Debug.WriteLine("It's seems that server was unable to start...");
                Stop("Start::Exception occured:" + ex.Message);
                throw;
            }
        }
        #endregion Public members

        #region Private members

        private static void WriteString(string msg, NetworkStream net)
        {
            var data = Encoding.ASCII.GetBytes(msg);
            var sz = BitConverter.GetBytes(data.Length);
            net.Write(sz, 0, sz.Length);
            net.Write(data, 0, data.Length);
            net.Flush();
        }

        private static byte[] ObjToByteArray<T>(T obj)
        {
            using (var ms = new MemoryStream())
            {
                var xmlS = new XmlSerializer(typeof(T));
                xmlS.Serialize(ms, obj);
                return ms.ToArray();
            }
        }

        private static void WriteData(byte[] msg, NetworkStream net)
        {
            var sz = BitConverter.GetBytes(msg.Length);
            net.Write(sz, 0, sz.Length);
            net.Write(msg, 0, msg.Length);
            net.Flush();
        }

        private static string ReadString(NetworkStream net)
        {
            var sz = new Byte[sizeof(Int32)];
            net.Read(sz, 0, sz.Length);
            var data = new Byte[BitConverter.ToInt32(sz, 0)];
            net.Read(data, 0, data.Length);
            return Encoding.ASCII.GetString(data);
        }

        /// <summary>
        /// Casts message to clients
        /// </summary>
        /// <param name="action">Message</param>
        /// <param name="nick">Nickname (initiator)</param>
        /// <param name="flag">Targets: 0 == broadcast (command), 1 == multicast (info or command to all but Nick), 2 == unicast (info or command for Nick only)</param>
        private void Cast(byte[] action, string nick, int flag)
        {
            lock (PoolClients)
            {
                foreach (DictionaryEntry item in PoolClients)
                {
                    if (nick == (string) item.Key && flag == 1)
                        continue;
                    if (nick != (string) item.Key && flag == 2)
                        continue;
                    var castSocket = ((poolClient) item.Value).ClientSocket;
                    if (!castSocket.Connected)
                        continue;
                    var castStream = castSocket.GetStream();
                    if (null == castStream)
                        continue;
                    WriteData(action, castStream);
                    Debug.WriteLine("Cast of action. From client:" + nick + " and flag is " + flag);
                    if ((string) item.Key == nick && flag == 2)
                        return;
                }
            }
        }

        /// <summary>
        /// Adds client to clients list and starts chat
        /// </summary>
        /// <param name="nick">Client nickname</param>
        /// <param name="net">NetworkStream to write in if neccessary</param>
        private void ConnectClient(string nick, NetworkStream net)
        {
            if (!PoolClients.ContainsKey(nick))
            {
                var client = new poolClient();
                client.Start(_clientSocket, nick, this);
                lock (PoolClients)
                {
                    PoolClients.Add(nick, client);
                }
                UpdateClientsCountGui();
                Cast("JOIN:" + nick, "SV", 1);
            }
            else
            {
                WriteString("#NICKDUP:SV:SV$", net);
                net.Close();
                _clientSocket.Close();
            }

        }

        /// <summary>
        /// Main clients accepting routine
        /// </summary>
        private void Listen()
        {
            try
            {
                while (_working)
                {
                    _clientSocket = _serverSocket.AcceptTcpClient();
                    if (!_clientSocket.Connected)
                        break;
                    var net = _clientSocket.GetStream();
                    var data = ReadString(net);
                    var idx = data.IndexOf("$", StringComparison.Ordinal);
                    if (idx >= 0)
                    {
                        data = data.Substring(0, idx);
                        DispatchConnectionRequest(data, net);
                    }
                }
                Stop("GetMsg::Stopped work normally");
            }
            catch (SocketException ex)
            {
                Stop("GetMsg::Clients' handling was interrupted. The reason is SocketException:" + ex.Message);
            }
        }

        /// <summary>
        /// Determines type of incoming messages and makes corresp.calls
        /// </summary>
        /// <param name="msg">Cleared message from client</param>
        /// <param name="net">NetworkStream to write if neccessary</param>
        private void DispatchConnectionRequest(string msg, NetworkStream net)
        {
            if (msg.StartsWith("#CNN"))
            {
                msg = msg.Substring(5);
                ConnectClient(msg, net);
            }
            if (msg.StartsWith("#SEARCH"))
            {
                WriteString("#WEBRD:SV:SV$", net);
                net.Close();
                _clientSocket.Close();
            }
        }
        #endregion Private members
    }

    #region poolClient
    /// <summary>
    /// Accepted TCP client to interact with in a separate thread
    /// </summary>
    public class poolClient
    {
        #region Private members and variables
        private weBoardServer _svr;
        private string _nick;
        private Thread _cli;

        private T ByteArrayToObj<T>(byte[] obj)
        {
            using (var ms = new MemoryStream(obj))
                return (T)(new XmlSerializer(typeof(T))).Deserialize(ms);
        }

        private byte[] ReadData(NetworkStream net)
        {
            var sz = new Byte[sizeof(Int32)];
            net.Read(sz, 0, sz.Length);
            var data = new Byte[BitConverter.ToInt32(sz, 0)];
            net.Read(data, 0, data.Length);
            return data;
        }

        /// <summary>
        /// Performs analyzing of message
        /// </summary>
        /// <param name="msg">Message to process</param>
        private void DispatchClientMessages(string msg)
        {
            if (msg.StartsWith("#DSC"))
            {
                msg = msg.Substring(5);
                _svr.DisconnectClient(msg);
                return;
            }
            Debug.WriteLine(_nick + ": " + msg);
            _svr.Cast(msg, _nick, 1);
        }

        /// <summary>
        /// Unpacks and then retranslates received (as byte array) weBoardAction
        /// </summary>
        /// <param name="data"></param>
        private void UnpackAction(byte[] data)
        {
            try
            {
                var act = ByteArrayToObj<weBoardAction>(data);
                _svr.Retranslate(act, _nick);
            }
            catch (Exception ex)
            {
                Debug.WriteLine("UnpackAction::Invalid action data. Exception is:" + ex.Message);
            }
        }

        /// <summary>
        /// Does actual message exchange by itself
        /// </summary>
        private void DoExchange()
        {
            //var sentCurrentPicture = false;
            while (ClientSocket.Connected)
            {
                try
                {
                    var net = ClientSocket.GetStream();
                    //TODO: solve TargetInvocationException issue
                    /*
                    if (!sentCurrentPicture)
                    {
                        Application.Current.Dispatcher.Invoke(new Action(() =>
                        {
                            foreach (var item in weBoardServer.Actions)
                            {
                                using (var ms = new MemoryStream())
                                {
                                    var xmlS = new XmlSerializer(typeof (weBoardAction));
                                    xmlS.Serialize(ms, item);
                                    var bytes = ms.ToArray();
                                    var sz = BitConverter.GetBytes(bytes.Length);
                                    net.Write(sz, 0, sz.Length);
                                    net.Write(bytes, 0, bytes.Length);
                                    net.Flush();
                                }
                            }
                        }));
                        ;
                        sentCurrentPicture = true;
                    }
                    */
                    var data = ReadData(net);
                    var msg = Encoding.ASCII.GetString(data);
                    if (msg == String.Empty)
                        throw new ArgumentNullException("NetworkStream Read error:zero bytes read");
                    var idx = msg.IndexOf("$", StringComparison.Ordinal);
                    if (idx >= 0)
                    {
                        msg = msg.Substring(0, idx);
                        DispatchClientMessages(msg);
                    }
                    else
                    {
                        UnpackAction(data);
                    }
                }
                catch (Exception ex)
                {
                    Debug.WriteLine("Exception occured:" + ex.Message + "\nClient " + _nick + " disconnected from server...");
                    Stop();
                }
            }
        }

        #endregion Private members and variables

        #region Public members and variables
        /// <summary>
        /// Socket should be visible to pool broker
        /// </summary>
        public TcpClient ClientSocket;

        /// <summary>
        /// Starts message exchange routine
        /// </summary>
        /// <param name="inSocket">Socket</param>
        /// <param name="name">Client nick</param>
        /// <param name="svr">Parent server</param>
        public void Start(TcpClient inSocket, string name, weBoardServer svr)
        {
            ClientSocket = inSocket;
            _nick = name;
            _svr = svr;
            _cli = new Thread(DoExchange) {Name = "poolClient[" + _nick + "]_Thread"};
            _cli.SetApartmentState(ApartmentState.STA);
            _cli.Start();
        }

        /// <summary>
        /// Stops message changing: closes socket and so on
        /// </summary>
        public void Stop()
        {
            if (!_cli.IsAlive)
                return;
            if (ClientSocket.Connected)
                ClientSocket.Close();
            lock (_svr.PoolClients)
            {
                _svr.PoolClients.Remove(_nick);
                _svr.UpdateClientsCountGui();    
            }
        }
        #endregion Public members and variables
    }
    #endregion poolClient
}